/*
 * © 2017 AgNO3 Gmbh & Co. KG
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 */
package jcifs.smb;

import java.io.IOException;
import java.util.Arrays;
import java.util.EnumSet;
import java.util.Locale;
import java.util.Objects;
import java.util.Random;
import java.util.Set;
import java.util.concurrent.atomic.AtomicLong;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import jcifs.CIFSContext;
import jcifs.CIFSException;
import jcifs.Configuration;
import jcifs.DfsReferralData;
import jcifs.RuntimeCIFSException;
import jcifs.SmbConstants;
import jcifs.SmbResourceLocator;
import jcifs.SmbTreeHandle;
import jcifs.internal.CommonServerMessageBlockRequest;
import jcifs.internal.CommonServerMessageBlockResponse;
import jcifs.internal.RequestWithPath;
import jcifs.internal.dfs.DfsReferralDataInternal;
import jcifs.internal.smb1.com.SmbComClose;
import jcifs.internal.smb1.com.SmbComFindClose2;
import jcifs.internal.smb1.trans.nt.NtTransQuerySecurityDesc;
import jcifs.util.transport.TransportException;

/**
 * This class encapsulates the logic for switching tree connections
 *
 * Switching trees can occur either when the tree has been disconnected by failure or idle-timeout - as well as on
 * DFS referrals.
 *
 * @author mbechler
 *
 */
class SmbTreeConnection implements AutoCloseable {

    private static final Logger log = LoggerFactory.getLogger(SmbTreeConnection.class);

    private final CIFSContext ctx;
    private final SmbTreeConnection delegate;
    private SmbTreeImpl tree;
    private volatile boolean treeAcquired;
    private volatile boolean delegateAcquired;

    private SmbTransportInternal exclusiveTransport;
    private boolean nonPooled;

    private final AtomicLong usageCount = new AtomicLong();

    private static final Random RAND = new Random();

    protected SmbTreeConnection(final CIFSContext ctx) {
        this.ctx = ctx;
        this.delegate = null;
    }

    protected SmbTreeConnection(final SmbTreeConnection treeConnection) {
        this.ctx = treeConnection.ctx;
        this.delegate = treeConnection;
    }

    static SmbTreeConnection create(final CIFSContext c) {
        if (c.getConfig().isTraceResourceUsage()) {
            return new SmbTreeConnectionTrace(c);
        }
        return new SmbTreeConnection(c);
    }

    static SmbTreeConnection create(final SmbTreeConnection c) {
        if (c.ctx.getConfig().isTraceResourceUsage()) {
            return new SmbTreeConnectionTrace(c);
        }
        return new SmbTreeConnection(c);
    }

    /**
     * @return the active configuration
     */
    public Configuration getConfig() {
        return this.ctx.getConfig();
    }

    private synchronized SmbTreeImpl getTree() {
        final SmbTreeImpl t = this.tree;
        if (t != null) {
            return t.acquire(false);
        }
        if (this.delegate != null) {
            this.tree = this.delegate.getTree();
            return this.tree;
        }
        return t;
    }

    /**
     * @return
     */
    private synchronized SmbTreeImpl getTreeInternal() {
        final SmbTreeImpl t = this.tree;
        if (t != null) {
            return t;
        }
        if (this.delegate != null) {
            return this.delegate.getTreeInternal();
        }
        return null;
    }

    /**
     * @param t
     */
    private synchronized void switchTree(final SmbTreeImpl t) {
        try (SmbTreeImpl old = getTree()) {
            if (old == t) {
                return;
            }
            final boolean wasAcquired = this.treeAcquired;
            log.debug("Switching tree");
            if (t != null) {
                log.debug("Acquired tree on switch " + t);
                t.acquire();
                this.treeAcquired = true;
            } else {
                this.treeAcquired = false;
            }

            this.tree = t;
            if ((old != null) && wasAcquired) {
                // release
                old.release(true);
            }
            if (this.delegate != null && this.delegateAcquired) {
                log.debug("Releasing delegate");
                this.delegateAcquired = false;
                this.delegate.release();
            }
        }
    }

    /**
     * @return tree connection with increased usage count
     */
    public SmbTreeConnection acquire() {
        final long usage = this.usageCount.incrementAndGet();
        if (log.isTraceEnabled()) {
            log.trace("Acquire tree connection " + usage + " " + this);
        }

        if (usage == 1) {
            synchronized (this) {
                try (SmbTreeImpl t = getTree()) {
                    if ((t != null) && !this.treeAcquired) {
                        if (log.isDebugEnabled()) {
                            log.debug("Acquire tree on first usage " + t);
                        }
                        t.acquire();
                        this.treeAcquired = true;
                    }
                }
                if (this.delegate != null && !this.delegateAcquired) {
                    log.debug("Acquire delegate on first usage");
                    this.delegate.acquire();
                    this.delegateAcquired = true;
                }
            }
        }

        return this;

    }

    /**
     *
     */
    public void release() {
        final long usage = this.usageCount.decrementAndGet();
        if (log.isTraceEnabled()) {
            log.trace("Release tree connection " + usage + " " + this);
        }

        if (usage == 0) {
            synchronized (this) {
                try (SmbTreeImpl t = getTree()) {
                    if (this.treeAcquired && t != null) {
                        if (log.isDebugEnabled()) {
                            log.debug("Tree connection no longer in use, release tree " + t);
                        }
                        this.treeAcquired = false;
                        t.release();
                    }
                }
                if (this.delegate != null && this.delegateAcquired) {
                    this.delegateAcquired = false;
                    this.delegate.release();
                }
            }

            final SmbTransportInternal et = this.exclusiveTransport;
            if (et != null) {
                synchronized (this) {
                    try {
                        log.debug("Disconnecting exclusive transport");
                        this.exclusiveTransport = null;
                        this.tree = null;
                        this.treeAcquired = false;
                        et.close();
                        et.disconnect(false, false);
                    } catch (final Exception e) {
                        log.error("Failed to close exclusive transport", e);
                    }
                }
            }
        } else if (usage < 0) {
            log.error("Usage count dropped below zero " + this);
            throw new RuntimeCIFSException("Usage count dropped below zero");
        }
    }

    /**
     * Close the tree connection (implements AutoCloseable)
     *
     * This method provides a standard way to close resources using try-with-resources.
     * It delegates to the release() method to properly clean up the connection.
     */
    @Override
    public void close() {
        if (log.isDebugEnabled()) {
            log.debug("Closing SmbTreeConnection via AutoCloseable.close()");
        }
        release();
    }

    protected void checkRelease() {
        if (isConnected() && this.usageCount.get() != 0) {
            log.warn("Tree connection was not properly released " + this);
        }
    }

    synchronized void disconnect(final boolean inError) {
        try (SmbSessionImpl session = getSession()) {
            if (session == null) {
                return;
            }
            try (SmbTransportImpl transport = session.getTransport()) {
                synchronized (transport) {
                    final SmbTreeImpl t = getTreeInternal();
                    if (t != null) {
                        try {
                            t.treeDisconnect(inError, true);
                        } finally {
                            this.tree = null;
                            this.treeAcquired = false;
                        }
                    } else {
                        this.delegate.disconnect(inError);
                    }
                }
            }
        }
    }

    <T extends CommonServerMessageBlockResponse> T send(final SmbResourceLocatorImpl loc, final CommonServerMessageBlockRequest request,
            final T response, final RequestParam... params) throws CIFSException {
        return send(loc, request, response,
                params.length == 0 ? EnumSet.noneOf(RequestParam.class) : EnumSet.copyOf(Arrays.asList(params)));
    }

    <T extends CommonServerMessageBlockResponse> T send(final SmbResourceLocatorImpl loc, final CommonServerMessageBlockRequest request,
            final T response, final Set<RequestParam> params) throws CIFSException {
        CIFSException last = null;
        final RequestWithPath rpath = request instanceof RequestWithPath r ? r : null;
        final String savedPath = rpath != null ? rpath.getPath() : null;
        final String savedFullPath = rpath != null ? rpath.getFullUNCPath() : null;

        final String fullPath = "\\" + loc.getServer() + "\\" + loc.getShare() + loc.getUNCPath();
        final int maxRetries = this.ctx.getConfig().getMaxRequestRetries();
        for (int retries = 1; retries <= maxRetries; retries++) {

            if (rpath != null) {
                rpath.setFullUNCPath(null, null, fullPath);
            }

            try {
                return send0(loc, request, response, params);
            } catch (final SmbException smbe) {
                // Retrying only makes sense if the invalid parameter is an tree id. If we have a stale file descriptor
                // retrying make no sense, as it will never become available again.
                if (params.contains(RequestParam.NO_RETRY)
                        || !(smbe.getCause() instanceof TransportException) && smbe.getNtStatus() != NtStatus.NT_STATUS_INVALID_PARAMETER) {
                    log.debug("Not retrying", smbe);
                    throw smbe;
                }
                log.debug("send", smbe);
                last = smbe;
            } catch (final CIFSException e) {
                log.debug("send", e);
                last = e;
            }
            // If we get here, we got the 'The Parameter is incorrect' error or a transport exception
            // Disconnect and try again from scratch.

            if (log.isDebugEnabled()) {
                log.debug(String.format("Retrying (%d/%d) request %s", retries, maxRetries, request));
            }

            // should we disconnect the transport here? otherwise we make an additional attempt to detect that if the
            // server closed the connection as a result
            log.debug("Disconnecting tree on send retry", last);
            disconnect(true);

            if (retries >= maxRetries) {
                break;
            }

            if (request != null) {
                log.debug("Resetting request");
                request.reset();
            }
            if (rpath != null) {
                // resolveDfs() and tree.send() modify the request packet.
                // I want to restore it before retrying. request.reset()
                // restores almost everything that was modified, except the path.
                rpath.setPath(savedPath);
                rpath.setFullUNCPath(rpath.getDomain(), rpath.getServer(), savedFullPath);
            }
            if (response != null) {
                response.reset();
            }

            try (SmbTreeHandle th = connectWrapException(loc)) {
                log.debug("Have new tree connection for retry");
            } catch (final SmbException e) {
                log.debug("Failed to connect tree on retry", e);
                last = e;
            }
        }

        if (last != null) {
            log.debug("All attempts have failed, last exception", last);
            throw last;
        }
        throw new SmbException("All attempts failed, but no exception");
    }

    private <T extends CommonServerMessageBlockResponse> T send0(final SmbResourceLocatorImpl loc,
            final CommonServerMessageBlockRequest request, final T response, final Set<RequestParam> params)
            throws CIFSException, DfsReferral {
        for (int limit = 10; limit > 0; limit--) {
            if (request instanceof RequestWithPath) {
                ensureDFSResolved(loc, (RequestWithPath) request);
            }
            try (SmbTreeImpl t = getTree()) {
                if (t == null) {
                    throw new CIFSException("Failed to get tree connection");
                }
                return t.send(request, response, params);
            } catch (final DfsReferral dre) {
                if (dre.getData().unwrap(DfsReferralDataInternal.class).isResolveHashes()) {
                    throw dre;
                }
                request.reset();
                log.trace("send0", dre);
            }
        }

        throw new CIFSException("Loop in DFS referrals");
    }

    /**
     * @param loc
     * @return tree handle
     * @throws SmbException
     */
    public SmbTreeHandleImpl connectWrapException(final SmbResourceLocatorImpl loc) throws SmbException {
        try {
            return connect(loc);
        } catch (final SmbException se) {
            throw se;
        } catch (final IOException ioe) {
            throw new SmbException("Failed to connect to server", ioe);
        }
    }

    /**
     * @param loc
     * @return tree handle
     * @throws IOException
     */
    public synchronized SmbTreeHandleImpl connect(final SmbResourceLocatorImpl loc) throws IOException {
        try (SmbSessionImpl session = getSession()) {
            if (isConnected()) {
                try (SmbTransportImpl transport = session.getTransport()) {
                    if (transport.isDisconnected() || transport.getRemoteHostName() == null) {
                        /*
                         * Tree/session thinks it is connected but transport disconnected
                         * under it, reset tree to reflect the truth.
                         */
                        log.debug("Disconnecting failed tree and session");
                        disconnect(true);
                    }
                }
            }

            if (isConnected()) {
                log.trace("Already connected");
                return new SmbTreeHandleImpl(loc, this);
            }

            return connectHost(loc, loc.getServerWithDfs());
        }

    }

    /**
     * @return whether we have a valid tree connection
     */
    @SuppressWarnings("resource")
    public synchronized boolean isConnected() {
        final SmbTreeImpl t = getTreeInternal();
        return t != null && t.isConnected();
    }

    /**
     *
     * @param loc
     * @param host
     * @return tree handle
     * @throws IOException
     */
    public synchronized SmbTreeHandleImpl connectHost(final SmbResourceLocatorImpl loc, final String host) throws IOException {
        return connectHost(loc, host, null);
    }

    /**
     *
     * @param loc
     * @param host
     * @param referral
     * @return tree handle
     * @throws IOException
     */
    public synchronized SmbTreeHandleImpl connectHost(final SmbResourceLocatorImpl loc, String host, final DfsReferralData referral)
            throws IOException {
        String targetDomain = ctx.getCredentials().getUserDomain();
        try (SmbTreeImpl t = getTree()) {
            if (t != null) {
                if (log.isDebugEnabled()) {
                    log.debug("Tree is " + t);
                }

                if (Objects.equals(loc.getShare(), t.getShare())) {
                    try (SmbSessionImpl session = t.getSession()) {
                        targetDomain = session.getTargetDomain();
                        if (!session.isFailed()) {
                            try (SmbTransportImpl trans = session.getTransport();
                                    SmbTreeImpl ct = connectTree(loc, host, t.getShare(), trans, t, null)) {
                                switchTree(ct);
                                return new SmbTreeHandleImpl(loc, this);
                            }
                        }
                        log.debug("Session no longer valid");
                    }
                }
            }
        }

        final String hostName = loc.getServerWithDfs();
        final String path = loc.getType() == SmbConstants.TYPE_SHARE || loc.getUNCPath() == null || "\\".equals(loc.getUNCPath()) ? null
                : loc.getUNCPath();
        String share = loc.getShare();

        final DfsReferralData start = referral != null ? referral : this.ctx.getDfs().resolve(this.ctx, hostName, loc.getShare(), path);
        DfsReferralData dr = start;
        IOException last = null;
        do {
            if (dr != null) {
                targetDomain = dr.getDomain();
                host = dr.getServer().toLowerCase(Locale.ROOT);
                share = dr.getShare();
            }

            try {

                if (this.nonPooled) {
                    if (log.isDebugEnabled()) {
                        log.debug("Using exclusive transport for " + this);
                    }
                    this.exclusiveTransport = this.ctx.getTransportPool()
                            .getSmbTransport(this.ctx, host, loc.getPort(), true, loc.shouldForceSigning())
                            .unwrap(SmbTransportInternal.class);
                    final SmbTransportInternal trans = this.exclusiveTransport;
                    try (SmbSessionInternal smbSession = trans.getSmbSession(this.ctx, host, targetDomain).unwrap(SmbSessionInternal.class);
                            SmbTreeImpl uct = smbSession.getSmbTree(share, null).unwrap(SmbTreeImpl.class);
                            SmbTreeImpl ct = connectTree(loc, host, share, trans, uct, dr)) {

                        if (dr != null) {
                            ct.setTreeReferral(dr);
                            if (dr != start) {
                                dr.unwrap(DfsReferralDataInternal.class).replaceCache();
                            }
                        }
                        switchTree(ct);
                        return new SmbTreeHandleImpl(loc, this);
                    }
                }

                try (SmbTransportInternal trans = this.ctx.getTransportPool()
                        .getSmbTransport(this.ctx, host, loc.getPort(), false, loc.shouldForceSigning())
                        .unwrap(SmbTransportInternal.class);
                        SmbSessionInternal smbSession = trans.getSmbSession(this.ctx, host, targetDomain).unwrap(SmbSessionInternal.class);
                        SmbTreeImpl uct = smbSession.getSmbTree(share, null).unwrap(SmbTreeImpl.class);
                        SmbTreeImpl ct = connectTree(loc, host, share, trans, uct, dr)) {
                    if (dr != null) {
                        ct.setTreeReferral(dr);
                        if (dr != start) {
                            dr.unwrap(DfsReferralDataInternal.class).replaceCache();
                        }
                    }
                    switchTree(ct);
                    return new SmbTreeHandleImpl(loc, this);
                }
            } catch (final IOException e) {
                last = e;
                log.debug("Referral failed, trying next", e);
            }

            if (dr != null) {
                dr = dr.next();
            }
        } while (dr != start);
        throw last;
    }

    /**
     * @param loc
     * @param addr
     * @param trans
     * @param t
     * @throws CIFSException
     */
    private SmbTreeImpl connectTree(final SmbResourceLocatorImpl loc, final String addr, final String share,
            final SmbTransportInternal trans, final SmbTreeImpl t, final DfsReferralData referral) throws CIFSException {
        if (log.isDebugEnabled() && trans.isSigningOptional() && !loc.isIPC() && !this.ctx.getConfig().isSigningEnforced()) {
            log.debug("Signatures for file enabled but not required " + this);
        }

        if (referral != null) {
            t.markDomainDfs();
        }

        try {
            if (log.isTraceEnabled()) {
                log.trace("doConnect: " + addr);
            }
            t.treeConnect(null, null);
            return t.acquire();
        } catch (final SmbAuthException sae) {
            log.debug("Authentication failed", sae);
            return retryAuthentication(loc, share, trans, t, referral, sae);
        }
    }

    private SmbTreeImpl retryAuthentication(final SmbResourceLocatorImpl loc, final String share, final SmbTransportInternal trans,
            final SmbTreeImpl t, final DfsReferralData referral, final SmbAuthException sae) throws SmbAuthException, CIFSException {
        try (SmbSessionImpl treesess = t.getSession()) {
            if (treesess.getCredentials().isAnonymous() || treesess.getCredentials().isGuest()) {
                // refresh anonymous session or fallback to anonymous from guest login
                try (SmbSessionInternal s =
                        trans.getSmbSession(this.ctx.withAnonymousCredentials(), treesess.getTargetHost(), treesess.getTargetDomain())
                                .unwrap(SmbSessionInternal.class);
                        SmbTreeImpl tr = s.getSmbTree(share, null).unwrap(SmbTreeImpl.class)) {
                    tr.treeConnect(null, null);
                    log.debug("Anonymous retry succeeded");
                    return tr.acquire();
                } catch (final Exception e) {
                    log.debug("Retry also failed", e);
                    throw sae;
                }
            } else {
                if (!this.ctx.renewCredentials(loc.getURL().toString(), sae)) {
                    throw sae;
                }
                log.debug("Trying to renew credentials after auth error");
                try (SmbSessionInternal s = trans.getSmbSession(this.ctx, treesess.getTargetHost(), treesess.getTargetDomain())
                        .unwrap(SmbSessionInternal.class); SmbTreeImpl tr = s.getSmbTree(share, null).unwrap(SmbTreeImpl.class)) {
                    if (referral != null) {
                        tr.markDomainDfs();
                    }
                    tr.treeConnect(null, null);
                    return tr.acquire();
                }
            }

        }
    }

    SmbResourceLocator ensureDFSResolved(final SmbResourceLocatorImpl loc) throws CIFSException {
        return ensureDFSResolved(loc, null);
    }

    SmbResourceLocator ensureDFSResolved(final SmbResourceLocatorImpl loc, final RequestWithPath request) throws CIFSException {
        if (request instanceof SmbComClose) {
            return loc;
        }

        for (int retries = 0; retries < 1 + this.ctx.getConfig().getMaxRequestRetries(); retries++) {
            try {
                return resolveDfs0(loc, request);
            } catch (final SmbException smbe) {
                // The connection may have been dropped?
                if (smbe.getNtStatus() != NtStatus.NT_STATUS_NOT_FOUND && !(smbe.getCause() instanceof TransportException)) {
                    throw smbe;
                }
                log.debug("resolveDfs", smbe);
            }
            // If we get here, we apparently have a bad connection.
            // Disconnect and try again.
            if (log.isDebugEnabled()) {
                log.debug("Retrying (" + retries + ") resolveDfs: " + request);
            }
            log.debug("Disconnecting tree on DFS retry");
            disconnect(true);

            try (SmbTreeHandle th = connectWrapException(loc)) {}
        }

        return loc;
    }

    private SmbResourceLocator resolveDfs0(final SmbResourceLocatorImpl loc, final RequestWithPath request) throws CIFSException {
        try (SmbTreeHandleImpl th = connectWrapException(loc);
                SmbSessionImpl session = th.getSession();
                SmbTransportImpl transport = session.getTransport();
                SmbTreeImpl t = getTree()) {
            transport.ensureConnected();

            final String rpath = request != null ? request.getPath() : loc.getUNCPath();
            final String rfullpath =
                    request != null ? request.getFullUNCPath() : '\\' + loc.getServer() + '\\' + loc.getShare() + loc.getUNCPath();
            if (t.isInDomainDfs() || !t.isPossiblyDfs()) {
                if (!t.isInDomainDfs()) {
                    log.trace("Not in DFS");
                    return loc;
                }
                // need to adjust request path
                final DfsReferralData dr = t.getTreeReferral();
                if (dr != null) {
                    if (log.isDebugEnabled()) {
                        log.debug(String.format("Need to adjust request path %s (full: %s) -> %s", rpath, rfullpath, dr));
                    }
                    final String dunc = loc.handleDFSReferral(dr, rpath);
                    if (request != null) {
                        request.setPath(dunc);
                    }
                    return loc;
                }

                // fallthrough to normal handling
                log.debug("No tree referral but in DFS");
            }

            if (request != null) {
                request.setFullUNCPath(session.getTargetDomain(), session.getTargetHost(), rfullpath);
            }

            // for standalone DFS we could be checking for a referral here, too
            DfsReferralData dr = this.ctx.getDfs().resolve(this.ctx, loc.getServer(), loc.getShare(), loc.getUNCPath());
            if (dr != null) {
                if (log.isDebugEnabled()) {
                    log.debug("Resolved " + rfullpath + " -> " + dr);
                }

                final String dunc = loc.handleDFSReferral(dr, rpath);
                if (request != null) {
                    request.setPath(dunc);
                }

                if (!t.getShare().equals(dr.getShare())) {
                    // this should only happen for standalone roots or if the DC/domain root lookup failed
                    IOException last;
                    final DfsReferralData start = dr;
                    do {
                        if (log.isDebugEnabled()) {
                            log.debug("Need to switch tree for " + dr);
                        }
                        try (SmbTreeHandleImpl nt = connectHost(loc, session.getTargetHost(), dr)) {
                            log.debug("Switched tree");
                            return loc;
                        } catch (final IOException e) {
                            log.debug("Failed to connect tree", e);
                            last = e;
                        }
                        dr = dr.next();
                    } while (dr != start);
                    throw new CIFSException("All referral tree connections failed", last);
                }

                return loc;
            } else {
                if (!t.isInDomainDfs() || (request instanceof NtTransQuerySecurityDesc) || (request instanceof SmbComClose)
                        || (request instanceof SmbComFindClose2)) {
                    log.trace("Not in DFS");
                    return loc;
                }
                if (log.isDebugEnabled()) {
                    log.debug("No referral available for  " + rfullpath);
                }
                throw new CIFSException("No referral but in domain DFS " + rfullpath);
            }

        }
    }

    /**
     * Use a exclusive connection for this tree
     *
     * If an exclusive connection is used the caller must make sure that the tree handle is kept alive,
     * otherwise the connection will be disconnected once the usage drops to zero.
     *
     * @param np
     *            whether to use an exclusive connection
     */
    void setNonPooled(final boolean np) {
        this.nonPooled = np;
    }

    /**
     * @return the currently connected tid
     */
    @SuppressWarnings("resource")
    public long getTreeId() {
        final SmbTreeImpl t = getTreeInternal();
        if (t == null) {
            return -1;
        }
        return t.getTreeNum();
    }

    /**
     *
     * Only call this method while holding a tree handle
     *
     * @return session that this file has been loaded through
     */
    @SuppressWarnings("resource")
    public SmbSessionImpl getSession() {
        final SmbTreeImpl t = getTreeInternal();
        if (t != null) {
            return t.getSession();
        }
        return null;
    }

    /**
     *
     * Only call this method while holding a tree handle
     *
     * @param cap
     * @return whether the capability is available
     * @throws SmbException
     */
    public boolean hasCapability(final int cap) throws SmbException {
        try (SmbSessionImpl s = getSession()) {
            if (s != null) {
                try (SmbTransportImpl transport = s.getTransport()) {
                    return transport.hasCapability(cap);
                }
            }
            throw new SmbException("Not connected");
        }
    }

    /**
     * Only call this method while holding a tree handle
     *
     * @return the connected tree type
     */
    public int getTreeType() {
        try (SmbTreeImpl t = getTree()) {
            return t.getTreeType();
        }
    }

    /**
     *
     * Only call this method while holding a tree handle
     *
     * @return the share we are connected to
     */
    public String getConnectedShare() {
        try (SmbTreeImpl t = getTree()) {
            return t.getShare();
        }
    }

    /**
     *
     * Only call this method while holding a tree handle
     *
     * @param other
     * @return whether the connection refers to the same tree
     */
    public boolean isSame(final SmbTreeConnection other) {
        try (SmbTreeImpl t1 = getTree(); SmbTreeImpl t2 = other.getTree()) {
            return t1 == t2;
        }
    }

}
